<?php

namespace app\common\service;



//require 'vendor/autoload.php';

use Qcloud\Cos\Client;
use Qcloud\Cos\Exception\ServiceResponseException;
use Exception  as ClientException;
use think\facade\Config;

/**
 * 腾讯云OSS上传工具类
 */
class TencentOssUploader
{
    /**
     * OSS客户端实例
     * @var Client
     */
    private $client;

    /**
     * 配置信息
     * @var array
     */
    private $config;

    /**
     * 构造函数
     *
     * @param array $config 配置信息
     * [
     *     'secretId'    => '您的secretId',
     *     'secretKey'   => '您的secretKey',
     *     'region'      => '存储桶区域',
     *     'bucket'      => '存储桶名称',
     *     'timeout'     => 超时时间(秒),
     *     'schema'      => 'http或https'
     * ]
     * @throws Exception 配置错误时抛出异常
     */
    public function __construct(array $config = [])
    {

        $this->config = array_merge([
            'region' => Config::get('admin.cos_region', 'ap-guangzhou'),
            'secretId' => Config::get('admin.cos_secret_id', ''),
            'secretKey' => Config::get('admin.cos_secret_key', ''),
            'bucket' => Config::get('admin.cos_bucket', ''),
            'timeout' => 60,
            'schema'  => 'https',
//            'connect_timeout' => 30,
//            'chunk_size' => 5242880, // 默认5MB分片
//            'sign_expire' => 3600, // 签名过期时间
        ], $config);
        // 验证必要配置
        $required = ['secretId', 'secretKey', 'region', 'bucket'];
        foreach ($required as $key) {
            if (!isset($this->config[$key]) || empty($this->config[$key])) {
                throw new \Exception("缺少必要配置: {$key}");
            }
        }
        // 初始化客户端
        $this->initClient();
    }

    /**
     * 初始化OSS客户端
     */
    private function initClient()
    {
        $this->client = new Client([
            'region' => $this->config['region'],
            'schema' => $this->config['schema'],
            'credentials' => [
                'secretId'  => $this->config['secretId'],
                'secretKey' => $this->config['secretKey']
            ],
            'timeout' => $this->config['timeout']
        ]);
    }

    /**
     * 上传本地文件到OSS
     *
     * @param string $localFilePath 本地文件路径
     * @param array $ossFilePath OSS上的文件路径(包含文件名)
     * @return array 上传结果
     */
    public function uploadFile( $localFilePath,  $ossFile): array
    {
        try {
            $ossFilePath = $ossFile['path'];
            // 检查本地文件
            if (!$this->checkLocalFile($localFilePath)) {
                return [
                    'success' => false,
                    'message' => "本地文件无效: {$localFilePath}"
                ];
            }

            // 执行上传
            $result = $this->client->putObject([
                'Bucket' => $this->config['bucket'],
                'Key'    => $ossFilePath,
                'Body'   => fopen($localFilePath, 'rb'),
            ]);

            return [
                'success' => true,
                'data' => [
                    'url'       => $result['Location'],
                    'etag'      => $result['ETag'],
                    'requestId' => $result['RequestId'],
                    'path'      => $ossFilePath,
                    'ossFile'      => $ossFile
                ],
                'message' => '文件上传成功'
            ];

        } catch (ServiceResponseException $e) {
            return $this->handleServiceException($e);
        } catch (ClientException $e) {
            return $this->handleClientException($e);
        } catch (Exception $e) {
            return [
                'success' => false,
                'message' => "上传失败: {$e->getMessage()}"
            ];
        }
    }

    /**
     * 处理HTTP上传的文件(表单上传)
     *
     * @param array $file 从$_FILES获取的文件信息
     * @param string $basePath OSS上的基础路径
     * @return array 上传结果
     */
    public function uploadHttpFile( $file,  $basePath = 'uploads/'): array
    {

        // 检查上传错误
        if ($file['error'] !== UPLOAD_ERR_OK) {
            return [
                'success' => false,
                'message' => $this->getUploadErrorMsg($file['error'])
            ];
        }

        // 生成存储路径
        $ossFilePath = $this->generateOssPath($file['name'], $basePath);

        // 调用上传方法
        return $this->uploadFile($file['tmp_name'], $ossFilePath);
    }

    /**
     * 分片上传大文件
     *
     * @param string $localFilePath 本地文件路径
     * @param string $ossFilePath OSS上的文件路径
     * @param int $partSize 分片大小(字节)，默认5MB
     * @return array 上传结果
     */
    public function uploadLargeFile( $localFilePath,  $ossFilePath, int $partSize = 5242880): array
    {
        try {
            // 检查本地文件
            if (!$this->checkLocalFile($localFilePath)) {
                return [
                    'success' => false,
                    'message' => "本地文件无效: {$localFilePath}"
                ];
            }

            // 初始化分片上传
            $uploadId = $this->initiateMultipartUpload($ossFilePath);
            trace('$uploadId$uploadId'.date("Y-m-d H:i:s").json_encode($uploadId,256));
            if (!$uploadId) {
                return [
                    'success' => false,
                    'message' => '初始化分片上传失败'
                ];
            }

            // 计算分片数量
            $fileSize = filesize($localFilePath);
            $partCount = (int)ceil($fileSize / $partSize);

            // 上传分片
            $uploadParts = [];
            for ($i = 0; $i < $partCount; $i++) {
                $offset = $i * $partSize;
                $length = min($partSize, $fileSize - $offset);

                $part = $this->uploadPart(
                    $localFilePath,
                    $ossFilePath,
                    $uploadId,
                    $i + 1,
                    $offset,
                    $length
                );
                trace('$part$part'.date("Y-m-d H:i:s").json_encode($part,256));
                if (!$part['success']) {
                    // 上传失败，取消分片上传
                    $this->abortMultipartUpload($ossFilePath, $uploadId);
                    return $part;
                }

                $uploadParts[] = $part['data'];
            }

            // 完成分片上传
            return $this->completeMultipartUpload(
                $ossFilePath,
                $uploadId,
                $uploadParts
            );

        } catch (Exception $e) {
            return [
                'success' => false,
                'message' => "大文件上传失败: {$e->getMessage()}"
            ];
        }
    }

    /**
     * 初始化分片上传
     *
     * @param string $ossFilePath
     * @return string|false 上传ID或false
     */
    private function initiateMultipartUpload(string $ossFilePath)
    {
        try {
            $result = $this->client->createMultipartUpload([
                'Bucket' => $this->config['bucket'],
                'Key'    => $ossFilePath,
            ]);

            return $result['UploadId'];
        } catch (\Exception $e) {
            return false;
        }
    }

    /**
     * 上传分片
     *
     * @param string $localFilePath
     * @param string $ossFilePath
     * @param string $uploadId
     * @param int $partNumber
     * @param int $offset
     * @param int $length
     * @return array
     */
    private function uploadPart( $localFilePath,  $ossFilePath,  $uploadId,
                                 int $partNumber, int $offset, int $length): array
    {
        try {
            $handle = fopen($localFilePath, 'rb');
            fseek($handle, $offset);
            $data = fread($handle, $length);
            fclose($handle);

            $result = $this->client->uploadPart([
                'Bucket'     => $this->config['bucket'],
                'Key'        => $ossFilePath,
                'UploadId'   => $uploadId,
                'PartNumber' => $partNumber,
                'Body'       => $data,
            ]);

            return [
                'success' => true,
                'data' => [
                    'PartNumber' => $partNumber,
                    'ETag'       => $result['ETag'],
                ]
            ];
        } catch (ServiceResponseException $e) {
            return $this->handleServiceException($e);
        } catch (ClientException $e) {
            return $this->handleClientException($e);
        } catch (Exception $e) {
            return [
                'success' => false,
                'message' => "分片上传失败: {$e->getMessage()}"
            ];
        }
    }

    /**
     * 完成分片上传
     *
     * @param string $ossFilePath
     * @param string $uploadId
     * @param array $uploadParts
     * @return array
     */
    private function completeMultipartUpload( $ossFilePath,  $uploadId,  $uploadParts): array
    {
        try {
            $result = $this->client->completeMultipartUpload([
                'Bucket'   => $this->config['bucket'],
                'Key'      => $ossFilePath,
                'UploadId' => $uploadId,
                'Parts'    => $uploadParts,
            ]);

            return [
                'success' => true,
                'data' => [
                    'url'       => $result['Location'],
                    'etag'      => $result['ETag'],
                    'requestId' => $result['RequestId'],
                    'path'      => $ossFilePath
                ],
                'message' => '大文件分片上传成功'
            ];
        } catch (ServiceResponseException $e) {
            return $this->handleServiceException($e);
        } catch (ClientException $e) {
            return $this->handleClientException($e);
        } catch (Exception $e) {
            return [
                'success' => false,
                'message' => "完成分片上传失败: {$e->getMessage()}"
            ];
        }
    }

    /**
     * 取消分片上传
     *
     * @param string $ossFilePath
     * @param string $uploadId
     * @return bool
     */
    private function abortMultipartUpload(string $ossFilePath, string $uploadId): bool
    {
        try {
            $this->client->abortMultipartUpload([
                'Bucket'   => $this->config['bucket'],
                'Key'      => $ossFilePath,
                'UploadId' => $uploadId,
            ]);
            return true;
        } catch (Exception $e) {
            return false;
        }
    }

    /**
     * 检查本地文件是否有效
     *
     * @param string $filePath
     * @return bool
     */
    private function checkLocalFile( $filePath): bool
    {

        return file_exists($filePath) && is_file($filePath) && is_readable($filePath);
    }

    /**
     * 生成OSS存储路径
     *
     * @param string $originalName 原始文件名
     * @param string $basePath 基础路径
     * @return array
     */
    public function generateOssPath( $originalName,  $basePath): array
    {

        // 处理基础路径，确保以/结尾
        $basePath = rtrim($basePath, '/') . '/';

        // 生成日期目录
        $dateDir = date('Ymd') . '/';

        // 生成唯一文件名
        $fileName = uniqid() . '_' . $originalName;
        return [
            'dateDir' => $dateDir,
            'fileName' => $fileName,
            'basePath' => $basePath,
            'path' => $basePath . $dateDir . $fileName,
        ];
    }

    /**
     * 获取上传错误信息
     *
     * @param int $errorCode
     * @return string
     */
    private function getUploadErrorMsg(int $errorCode): string
    {
        $errors = [
            UPLOAD_ERR_INI_SIZE   => '上传文件超过了php.ini中upload_max_filesize选项限制的值',
            UPLOAD_ERR_FORM_SIZE  => '上传文件超过了HTML表单中MAX_FILE_SIZE选项指定的值',
            UPLOAD_ERR_PARTIAL    => '文件只有部分被上传',
            UPLOAD_ERR_NO_FILE    => '没有文件被上传',
            UPLOAD_ERR_NO_TMP_DIR => '找不到临时文件夹',
            UPLOAD_ERR_CANT_WRITE => '文件写入失败',
            UPLOAD_ERR_EXTENSION  => '文件上传被PHP扩展阻止'
        ];

        return $errors[$errorCode] ?? "文件上传错误，错误代码: {$errorCode}";
    }

    /**
     * 处理服务端异常
     *
     * @param ServiceResponseException $e
     * @return array
     */
    private function handleServiceException(ServiceResponseException $e): array
    {
        return [
            'success' => false,
            'message' => "OSS服务错误: {$e->getMessage()}",
            'code' => $e->getStatusCode(),
            'requestId' => $e->getRequestId()
        ];
    }

    /**
     * 处理客户端异常
     *
     * @param ClientException $e
     * @return array
     */
    private function handleClientException(ClientException $e): array
    {
        return [
            'success' => false,
            'message' => "客户端错误: {$e->getMessage()}"
        ];
    }
}
?>